package com.huangyi.adminservice.house.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.huangyi.adminapi.config.domain.dto.DictionaryDataDTO;
import com.huangyi.adminapi.house.domain.dto.DeviceDTO;
import com.huangyi.adminapi.house.domain.dto.SearchHouseListReqDTO;
import com.huangyi.adminapi.house.domain.dto.TagDTO;
import com.huangyi.adminservice.config.domain.entity.SysDictionaryData;
import com.huangyi.adminservice.config.mapper.SysDictionaryDataMapper;
import com.huangyi.adminservice.config.service.ISysDictionaryService;
import com.huangyi.adminservice.house.domain.dto.*;
import com.huangyi.adminservice.house.domain.entity.*;
import com.huangyi.adminservice.house.enums.HouseStatusEnum;
import com.huangyi.adminservice.house.mapper.*;
import com.huangyi.adminservice.house.service.IHouseService;
import com.huangyi.adminservice.house.service.filter.IHouseFilter;
import com.huangyi.adminservice.house.service.strategy.ISortStrategy;
import com.huangyi.adminservice.house.service.strategy.SortStrategyFactory;
import com.huangyi.adminservice.map.domain.entity.SysRegion;
import com.huangyi.adminservice.map.mapper.RegionMapper;
import com.huangyi.adminservice.user.domain.entity.AppUser;
import com.huangyi.adminservice.user.mapper.AppUserMapper;
import com.huangyi.commoncore.domain.dto.BasePageDTO;
import com.huangyi.commoncore.utils.BeanCopyUtil;
import com.huangyi.commoncore.utils.JsonUtil;
import com.huangyi.commoncore.utils.TimestampUtil;
import com.huangyi.commondomain.domain.ResultCode;
import com.huangyi.commondomain.exception.ServiceException;
import com.huangyi.commonredis.service.RedisService;
import com.huangyi.commonredis.service.RedissonLockService;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 房源服务实现类。
 * <p>
 * 主要职责：
 * <ul>
 *   <li>新增/编辑房源主信息</li>
 *   <li>维护城市-房源、标签-房源映射（MySQL & Redis）</li>
 *   <li>参数与字典项校验、地址合法性校验、去重校验</li>
 * </ul>
 */
@Slf4j
@Service
public class HouseServiceImpl implements IHouseService {

    /** 城市房源列表缓存前缀（Redis list key 前缀：house:list:{cityId}） */
    private static final String CITY_HOUSE_PREFIX = "house:list:";

    private static final String HOUSE_PREFIX = "house:";

    private static final String LOCK_KEY = "ScheduledTask:lock";

    /** 用户（房东）表 Mapper，用于校验房东是否存在 */
    @Autowired
    private AppUserMapper appUserMapper;

    /** 房源主表 Mapper（insertOrUpdate、exists 等） */
    @Autowired
    private HouseMapper houseMapper;

    /** 行政区划 Mapper，用于校验城市/区域合法性与上下级关系 */
    @Autowired
    private RegionMapper regionMapper;

    /** 标签表 Mapper，用于校验标签码是否存在 */
    @Autowired
    private TagMapper tagMapper;

    /** 字典数据 Mapper，用于设备、出租方式、朝向、居室等字典项校验 */
    @Autowired
    private SysDictionaryDataMapper sysDictionaryDataMapper;

    /** 标签-房源映射表 Mapper */
    @Autowired
    private TagHouseMapper tagHouseMapper;

    /** 房源状态表 Mapper（新增上架状态等） */
    @Autowired
    private HouseStatusMapper houseStatusMapper;

    /** 城市-房源映射表 Mapper（插入城市与房源的关联） */
    @Autowired
    private CityHouseMapper cityHouseMapper;

    /** Redis 服务 */
    @Autowired
    private RedisService redisService;

    @Autowired
    private ISysDictionaryService sysDictionaryService;

    @Autowired
    private RedissonLockService redissonLockService;


    @Autowired
    private final Map<String, IHouseFilter> houseFilterMap = new HashMap<>();

    /**
     * 新增或修改房源。
     *
     * <p>流程：
     * <ol>
     *   <li>参数与字典校验</li>
     *   <li>组装 House 实体</li>
     *   <li>编辑场景：根据变化修改城市/标签映射</li>
     *   <li>插入或更新 House；若为新增，补充 HouseStatus、CityHouse、TagHouse</li>
     *   <li>缓存房源</li>
     * </ol>
     *
     * @param reqDTO 新增或修改源请求
     * @return 房源 ID（新增时为自增 ID，修改时为原 ID）
     * @throws ServiceException 任何业务校验失败均抛出
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Long addOrEdit(HouseAddOrEditReqDTO reqDTO) {
        // 1. 校验参数
        checkAddOrEdit(reqDTO);

        // 2. 设置房源信息
        House house = setHouseInformation(reqDTO);

        // 3. 编辑场景：检查城市/标签是否需要变更（仅当存在 houseId）
        if (reqDTO.getHouseId() != null) {
            house.setId(reqDTO.getHouseId());

            House existHouse = houseMapper.selectById(reqDTO.getHouseId());
            if (cityHouseNeedChange(existHouse, reqDTO.getCityId())) {
                editCityHouses(house.getId(),existHouse.getCityId(),reqDTO.getCityId(),reqDTO.getCityName());
            }

            List<TagHouse> tagHouses = tagHouseMapper.selectList(
                    new LambdaQueryWrapper<TagHouse>().eq(TagHouse::getHouseId, reqDTO.getHouseId())
            );
            if (tagHouseNeedChange(tagHouses, reqDTO.getTagCodes())) {
                editTagHouses(house.getId(),tagHouses,reqDTO.getTagCodes());
            }
        }

        // 持久化 House（insert or update）
        houseMapper.insertOrUpdate(house);

        // 4. 新增时补充状态与映射（需要 houseId）
        if (reqDTO.getHouseId() == null) {
            HouseStatus houseStatus = new HouseStatus();
            houseStatus.setStatus(HouseStatusEnum.UP.name());
            houseStatus.setHouseId(house.getId());
            houseStatusMapper.insert(houseStatus);

            // MySQL + Redis：城市映射与缓存
            addCityHouses(house.getId(), house.getCityId(), house.getCityName());

            // MySQL：标签映射
            addTagHouses(house.getId(), reqDTO.getTagCodes());
        }

        // 5. 设置/刷新房源缓存
        cacheHouse(house.getId());

        return house.getId();
    }

    /**
     * 根据 houseId 查询房源信息
     * @param houseId 所查房源信息
     * @return 房源全部信息DTO
     */
    @Override
    public HouseDTO detail(Long houseId) {

        if(houseId == null || houseId < 0){
            log.warn("要查询的房源id为空或无效！ houseId:{}",houseId);
        }

        // 查缓存询
        HouseDTO houseDTO = getCacheHouse(houseId);

        // 判断缓存是否存在, 不存在则继续逻辑
        if(houseDTO != null){
            return houseDTO;
        }

        // 缓存不存在查数据库
        houseDTO = getHouseDTObyId(houseId);

        // 数据库不存在设置空缓存
        if(houseDTO == null){
            log.warn("查询的房源在数据库中不存在！");
            cacheNullHouse(houseId,60L, TimeUnit.SECONDS);
            return null;
        }

        // 查到就缓存数据
        cacheHouse(houseDTO);

        // 返回
        return houseDTO;

    }

    /**
     *
     * @param reqDTO
     * @return
     */
    @Override
    public BasePageDTO<HouseDescDTO> list(HouseListReqDTO reqDTO) {

        BasePageDTO<HouseDescDTO> result = new BasePageDTO<>();
        // 查询设计 house, status_house 两张表, 联表查询
        Long total = houseMapper.selectCountWithStatus(reqDTO);
        if(total == 0){
            log.warn("查询的列表总数为 0 houseListReqDTO:{}",JsonUtil.obj2String(reqDTO));
            result.setTotals(0);
            result.setTotalPages(0);
            result.setList(Arrays.asList());
            return result;
        }

        List<HouseDescDTO> houseDescDTOS = houseMapper.selectPageWithStatus(reqDTO);


        // 查询总数
        result.setTotals(
                Integer.parseInt(
                        String.valueOf(total)));

        // 设置总页数
        result.setTotalPages(
                BasePageDTO
                        .calculateTotalPages(total,reqDTO.getPageSize()));

        if(CollectionUtils.isEmpty(houseDescDTOS)){
            log.warn("超出查询房源列表范围 houseListReqDTO:{}",JsonUtil.obj2String(reqDTO));
            result.setList(Arrays.asList());
            return result;
        }

        // 设置List
        result.setList(houseDescDTOS);

        return result;
    }

    /**
     * 修改房源状态信息
     * @param reqDTO 房源状态修改信息DTO
     */
    @Override
    public void editStatus(HouseStatusEditReqDTO reqDTO) {
        // 校验是否为 null -> 校验是否合法 -> 修改
        // 进行房源 id 判断
        if(reqDTO.getHouseId() == null || reqDTO.getHouseId() < 0){
            throw new ServiceException("房源不存在，修改房源状态失败");
        }
        // 查询数据库 判断结果是否为空
        House house = houseMapper.selectById(reqDTO.getHouseId());
        if(house == null){
            throw new ServiceException("房源不存在, 修改房源状态失败");
        }

        // 查询房源状态表, 判断是否做了映射, 如果有映射 房源状态是否为空, 因为涉及到修改的都要先进行严密的检查
        HouseStatus houseStatus = houseStatusMapper.selectOne(
                new LambdaQueryWrapper<HouseStatus>()
                        .eq(HouseStatus::getHouseId, house.getId())
        );
        if(houseStatus == null || StringUtils.isEmpty(houseStatus.getStatus())){
            throw new ServiceException("房源状态不存在, 修改房源状态失败");
        }

        // 判断传入的数据是否合法
        HouseStatusEnum statusEnum = HouseStatusEnum.getByName(reqDTO.getStatus());
        if(statusEnum == null){
            throw new ServiceException("传入的房源状态有误, 修改房源状态失败！");
        }

        houseStatus.setStatus(reqDTO.getStatus());
        if(reqDTO.getStatus()
                .equalsIgnoreCase(HouseStatusEnum.RENTING.name())){

            if(StringUtils.isEmpty(reqDTO.getRentTimeCode())){
                throw new ServiceException("出租时长不能为空，修改房源状态失败！");
            }
            houseStatus.setRentTimeCode(reqDTO.getRentTimeCode());
            houseStatus.setRentStartTime(TimestampUtil.getCurrentMillis());
            switch (reqDTO.getRentTimeCode()){
                case "one_year"-> houseStatus.setRentEndTime(TimestampUtil.getYearLaterMillis(1L));
                case "half_year"-> houseStatus.setRentEndTime(TimestampUtil.getMonthsLaterMillis(6L));
                case "thirty_seconds"-> houseStatus.setRentEndTime(TimestampUtil.getSecondsLaterMillis(30L));
                case "three_month" -> houseStatus.setRentEndTime(TimestampUtil.getMonthsLaterMillis(3L));
                default -> throw new ServiceException("传入出租时长有误, 修改房源状态失败！");
            }

        }

        // 修改 房源状态映射表信息
        houseStatusMapper.updateById(houseStatus);

        // 在 redis 进行缓存
        cacheHouse(house.getId());
    }

    /**
     * 查询缓存房源信息
     * @param houseId 查询房源ID
     * @return 缓存中的房源信息
     */
    private HouseDTO getCacheHouse(Long houseId) {
        if(houseId == null || houseId < 0){
            log.warn("要查询的房源id为空或无效！ houseId:{}",houseId);
            return null;
        }
        HouseDTO houseDTO = null;
        try{
            String houseDTOString = redisService.getCacheObject(HOUSE_PREFIX + houseId, String.class);
            if(StringUtils.isBlank(houseDTOString)){
                log.warn("缓存中不存在所查房源 id 为:{}的信息",houseId);
                return null;
            }
            houseDTO = JsonUtil.string2Obj(houseDTOString, HouseDTO.class);
        }catch (Exception e){
            log.warn("查询Redis缓存失败, key:{}", HOUSE_PREFIX + houseId, e);
        }
        return houseDTO;
    }




    /**
     * 修改标签房源映射关系
     * @param houseId 修改的房源ID
     * @param oldTagHouses 老标签列表 存在TagHouse中
     * @param newTagCodes 新标签列表 存在 List<String> 中
     */
    private void editTagHouses(Long houseId, List<TagHouse> oldTagHouses,List<String> newTagCodes) {

        // 过滤出需要删除的标签
        Set<String> oldTagCodes = oldTagHouses.stream()
                .map(TagHouse::getTagCode)
                .collect(Collectors.toSet());
        List<String> deleteTagCodes = oldTagCodes.stream()
                .filter(oldTagCode ->!newTagCodes.contains(oldTagCode))
                .collect(Collectors.toList());

        // 删除需要删除的标签
        if(!CollectionUtils.isEmpty(deleteTagCodes)){
            tagHouseMapper.delete(
                    new LambdaQueryWrapper<TagHouse>()
                            .eq(TagHouse::getHouseId, houseId)
                            .eq(TagHouse::getTagCode, deleteTagCodes)//TODO
            );
        }

        // 过滤出需要新增的标签
        List<TagHouse> newTagHouses = newTagCodes.stream()
                .filter(newTagCode -> !oldTagCodes.contains(newTagCode))
                .map(newTagCode ->{
                    TagHouse tagHouse = new TagHouse();
                    tagHouse.setHouseId(houseId);
                    tagHouse.setTagCode(newTagCode);
                    return tagHouse;
                }).collect(Collectors.toList());

        // 更新需要更新的标签
        tagHouseMapper.insert(newTagHouses);
    }

    /**
     * 判断是否需要更新标签
     * @param oldTags 对象列表含有老标签信息
     * @param newTagCodes 新标签列表信息
     * @return 是否需要更新标签
     */
    private boolean tagHouseNeedChange(List<TagHouse> oldTags, List<String> newTagCodes) {
        List<String> oldTagCods = oldTags.stream()
                .map(TagHouse::getTagCode)
                .sorted() //排序
                .collect(Collectors.toList());
        newTagCodes.stream().sorted().collect(Collectors.toList());
        return !Objects.equals(oldTagCods, newTagCodes);
    }

    /**
     * 修改城市房源映射
     * @param houseId 新房源ID
     * @param oldCityId 老城市ID
     * @param newCityId 新城市ID
     * @param cityName 新城市名字
     */
    private void editCityHouses(Long houseId, Long oldCityId, Long newCityId, String cityName) {

        // 删除老映射
        cityHouseMapper.delete(
                new LambdaQueryWrapper<CityHouse>()
                        .eq(CityHouse::getCityId,oldCityId)
                        .eq(CityHouse::getHouseId,houseId)
        );

        // 增加新映射
        CityHouse cityHouse = new CityHouse();
        cityHouse.setHouseId(houseId);
        cityHouse.setCityId(oldCityId);
        cityHouse.setCityName(cityName);
        cityHouseMapper.insert(cityHouse);

        // 修改缓存
        cacheCityHouse(2,houseId, oldCityId,newCityId);

    }

    /**
     * 判断是否需要更新房源映射关系
     * @param oldHouse 老房源信息
     * @param newCityId 新城市ID
     * @return 是否需要更新
     */
    private boolean cityHouseNeedChange(House oldHouse, @NotNull(message = "城市id不能为空！") Long newCityId) {
        return !oldHouse.getCityId().equals(newCityId);
    }

    /**
     * 缓存房源信息
     * @param houseId 房源ID
     */
    @Override
    public void cacheHouse(Long houseId) {
        if(houseId == null){
            log.warn("查询的房源ID不存在 houseId:{}",houseId);
            return;
        }

        // 查询 HouseDTO
        HouseDTO houseDTO = getHouseDTObyId(houseId);
        if(houseDTO == null){
            log.warn("查询房源操作失败");
            return;
        }
        // 缓存
        cacheHouse(houseDTO);
    }

    /**
     * 根据用户id, 获取该用户下的全部房源id
     * @param userId 用户id
     * @return 该用户下的全部房源id
     */
    @Override
    public List<Long> listByUserId(Long userId) {
        if(userId == null){
            log.warn("房东id为空");
            return Arrays.asList();
        }

        List<House> houses = houseMapper.selectList(
                new LambdaQueryWrapper<House>()
                        .eq(House::getUserId, userId)
        );

        return houses.stream()
                .map(House::getId)
                .distinct()
                .collect(Collectors.toList());
    }

    /**
     * 刷新全量房源缓存
     */
    @Override
    public void refreshHouseIds() {

        // 查询全量城市列表(2级城市, MySQL)
        List<SysRegion> sysRegions = regionMapper.selectList(
                new LambdaQueryWrapper<SysRegion>()
                        .eq(SysRegion::getLevel,2)
        );

        for(SysRegion sysRegion : sysRegions){
            // 删除当前城市下的所有房源映射关系(Redis), 一个城市对应一个映射队列, 删除该城市对应的映射关系即可
            Long cityId = sysRegion.getId();
            redisService.removeForAllList(CITY_HOUSE_PREFIX + cityId);

            // 查询当前城市下所有的房源列表(MySQL), 一个城市对应多个房源, 需要用列表价接受 cityHouses
            List<CityHouse> cityHouses = cityHouseMapper.selectList(
                    new LambdaQueryWrapper<CityHouse>()
                            .eq(CityHouse::getCityId, cityId)
            );

            // 新增当前城市下的所有房源映射关系(Redis), 一个城市对应多个房源, cityHouses 是列表需要挨个处理,
            // 因为只需要缓存 houseId, 所以需要 map 遍历筛选。
            if(!CollectionUtils.isEmpty(cityHouses)){
                redisService.setCacheList(CITY_HOUSE_PREFIX + cityId,
                        cityHouses.stream().map(CityHouse::getHouseId)
                                .distinct()
                                .collect(Collectors.toList()));
            }

            // 更新房源列表详细信息(Redis)
            for(CityHouse cityHouse : cityHouses){
                cacheHouse(cityHouse.getHouseId());
            }
        }


    }


    /**
     * 查询房源列表，支持筛选、排序、翻页
     */
    @Override
    public BasePageDTO<HouseDTO> searchList(SearchHouseListReqDTO searchHouseListReqDTO) {
        // 获取城市下的全量房源列表
        List<HouseDTO> houseDTOs = getCacheHouseListByCity(searchHouseListReqDTO.getCityId());

        // 筛选、排序、分页, 传入城市下的全量房源列表 和 筛选条件
        return filterHouse(houseDTOs, searchHouseListReqDTO);
    }

    /**
     * 筛选、排序、分页, 城市下的全量房源列表
     * @param houseDTOList 该城市下的全量房源列表
     * @param searchHouseListReqDTO 筛选、排序、分页条件
     * @return 筛选后的房源列表信息
     */
    private BasePageDTO<HouseDTO> filterHouse(List<HouseDTO> houseDTOList, SearchHouseListReqDTO searchHouseListReqDTO) {
        // 筛选（多策略，全策略都要过一遍）
        List<HouseDTO> validHouseDTOList = houseFilter(houseDTOList, searchHouseListReqDTO);

        // 排序（多策略，只需要指定一个策略执行即可）
        validHouseDTOList = houseSorting(validHouseDTOList, searchHouseListReqDTO);

        // 分页
        return housePage(validHouseDTOList, searchHouseListReqDTO);
    }

    /**
     * 对房源列表进行分页
     * @param houseDTOList 排序后的房源列表
     * @param reqDTO 分页条件
     * @return 分页后的房源列表
     */
    private BasePageDTO<HouseDTO> housePage(List<HouseDTO> houseDTOList,
                                            SearchHouseListReqDTO reqDTO) {

        List<HouseDTO> pagedHouseDTOList = houseDTOList.stream()
                .skip(reqDTO.getOffset())
                .limit(reqDTO.getPageSize())// skip() + limit 求出需要有多少个数据, 也就是从偏移量 + 分页数量
                .collect(Collectors.toList());
        BasePageDTO<HouseDTO> result = new BasePageDTO<>();
        result.setTotals(houseDTOList.size());
        result.setTotalPages(
                BasePageDTO.calculateTotalPages(houseDTOList.size(), reqDTO.getPageSize()));// 通过总数 + 分页数量求出多少页
        result.setList(pagedHouseDTOList);
        return result;
    }

    /**
     * 对房源列表进行排序
     * @param validHouseDTOList 被筛选后有效的房源列表
     * @param searchHouseListReqDTO 排序条件
     * @return 根据条件排序后的房源列表
     */
    private List<HouseDTO> houseSorting(List<HouseDTO> validHouseDTOList,
                                        SearchHouseListReqDTO searchHouseListReqDTO) {
        // 多策略，只需要指定一个策略执行即可
        // 工厂模式：工厂根据指定要求给我生产出一个策略即可
        ISortStrategy sortStrategy = SortStrategyFactory.getSortStrategy(searchHouseListReqDTO.getSort());
        return sortStrategy.sort(validHouseDTOList, searchHouseListReqDTO);
    }

    /**
     * 筛选全量房源列表
     * @param houseDTOList 全量房源列表
     * @param searchHouseListReqDTO 筛选条件
     * @return 筛选后的房源列表
     */
    private List<HouseDTO> houseFilter(List<HouseDTO> houseDTOList,
                                       SearchHouseListReqDTO searchHouseListReqDTO) {
        return houseDTOList.stream()
                .filter(houseDTO -> houseFilterMap.values().stream()
                        .allMatch(houseFilter ->{
                            try{
                                return houseFilter.filter(houseDTO, searchHouseListReqDTO);
                            }catch (Exception e){
                                log.error("过滤房源发生异常, houseDTO:{}, filter：{}", JsonUtil.obj2String(houseDTO),
                                        houseFilter.getClass().getName(), e);
                                return false;
                            }
                        })
                ).collect(Collectors.toList());
    }

    /**
     * 根据城市id获取该城市下全量房源列表
     * @param cityId 城市id
     * @return 该城市下全量房源列表
     */
    private List<HouseDTO> getCacheHouseListByCity(Long cityId) {
        if(cityId == null){
            log.error("传入的城市id为空, 不能获取房源列表！");
            return Arrays.asList();
        }
        List<HouseDTO> result = new ArrayList<>();
        // 去缓存 Redis 中查城市房源映射关系
        List<Long> houseIds = getCacheCityHouses(cityId);

        // 获取根据城市房源映射关系 获取全量房源信息
        Set<Long> houseIdSet = new HashSet<>(houseIds);
        for(Long houseId : houseIdSet){
            // 根据 houseId 查询房源信息,
            // 查缓存询-缓存不存在查数据库-数据库不存在设置空缓存-数据库查到就缓存数据
            HouseDTO houseDTO = detail(houseId);
            if(houseDTO != null){
                result.add(houseDTO);
            }
        }

        return result;
    }

    private List<Long> getCacheCityHouses(Long cityId) {
        if(cityId == null){
            log.error("传入的城市id为空, 不能获取房源列表！");
            return Arrays.asList();
        }

        List<Long> houseIds = new ArrayList<>();
        try {
            houseIds = redisService.getCacheList(CITY_HOUSE_PREFIX + cityId, Long.class);
        } catch (Exception e) {
            log.error("从缓存中获取城市下的房源列表异常，key:{}",CITY_HOUSE_PREFIX + cityId, e);
        }
        return houseIds;

    }

    /**
     * 缓存房源信息
     * @param house 房源DTO
     */
    private void cacheHouse(HouseDTO house) {
        if(house == null){
            log.warn("缓存房源信息为空");
            return;
        }
        try{
            redisService.setCacheObject(HOUSE_PREFIX + house.getHouseId(),
                    JsonUtil.obj2String(house));
        }catch (Exception e){
            log.error("缓存房源完整信息时发生异常: houseDTO{}",JsonUtil.obj2String(house), e);
        }

    }

    /**
     * 缓存房源信息（带过期时间）
     * @param house 房源DTO
     * @param time 存活时间
     * @param timeUnit 存活时间单位
     */
    private void cacheHouse(HouseDTO house, Long time, TimeUnit timeUnit) {
        if(house == null){
            log.warn("缓存房源信息为空");
            return;
        }
        try{
            redisService.setCacheObject(HOUSE_PREFIX + house.getHouseId(),
                    JsonUtil.obj2String(house), time, timeUnit);
        }catch (Exception e){
            log.error("缓存房源完整信息时发生异常: houseDTO{}",JsonUtil.obj2String(house), e);
        }

    }

    /**
     * 缓存房源空信息（带过期时间）
     * @param houseId 房源DTO
     * @param time 存活时间
     * @param timeUnit 存活时间单位
     */
    private void cacheNullHouse(Long houseId, Long time, TimeUnit timeUnit) {
        if(houseId == null){
            log.warn("缓存房源信息为空");
            return;
        }
        try{
            redisService.setCacheObject(HOUSE_PREFIX + houseId,
                    JsonUtil.obj2String(new HouseDTO()), time, timeUnit);
        }catch (Exception e){
            log.error("缓存空房源完整信息时发生异常: houseId:{}",houseId, e);
        }

    }

    /**
     * 获取房源信息
     * @param houseId 房源ID
     * @return 房源全信息DTO
     */
    private HouseDTO getHouseDTObyId(Long houseId) {
        if(houseId == null){
            log.warn("查询的房源ID不存在 houseId:{}",houseId);
            return null;
        }
        // 根据 houseId 查 app_user, house, house_status
        House house = houseMapper.selectById(houseId);
        if(house == null){
            log.warn("查询房源信息操作失败 houseId:{}",houseId);
            return null;
        }

        AppUser appUser = appUserMapper.selectById(house.getUserId());
        if(appUser == null){
            log.warn("查询房主信息操作失败 houseId:{} , userID:{}",houseId,house.getUserId());
            return null;
        }

        HouseStatus houseStatus = houseStatusMapper.selectOne(
                new LambdaQueryWrapper<HouseStatus>()
                        .eq(HouseStatus::getHouseId, houseId)
        );

        List<TagHouse> tagHouses = tagHouseMapper.selectList(
                new LambdaQueryWrapper<TagHouse>()
                        .eq(TagHouse::getHouseId, houseId)
        );
        // 组装房源全信息, tagHouses, tag_house, dictionary_data
        return convertToHouseDTO(house,appUser,houseStatus,tagHouses);
    }

    private HouseDTO convertToHouseDTO(House house, AppUser appUser, HouseStatus houseStatus, List<TagHouse> tagHouses) {
        if(house == null || appUser == null || houseStatus == null ){
            log.warn("房源信息不完整");
            return null;
        }
        HouseDTO houseDTO = new HouseDTO();
        BeanUtils.copyProperties(house, houseDTO);
        BeanUtils.copyProperties(houseStatus, houseDTO);
        BeanUtils.copyProperties(appUser, houseDTO);

        houseDTO.setArea(house.getArea().doubleValue());
        houseDTO.setPrice(house.getPrice().doubleValue());
        houseDTO.setLongitude(house.getLongitude().doubleValue());
        houseDTO.setLatitude(house.getLatitude().doubleValue());
        houseDTO.setImages(JsonUtil.string2List(house.getImages(), String.class));

        // 处理 设备列表, 主要逻辑涉及 1. house 的 diverse 字段转成 List<String> 数据类型用于调用 sys_dictionary_data 表接口
        //                        2. sys_dictionary_data -> 通过 house 的 devices_list 字段获取数据,
        //                              类型为 List<DictionaryDataDTO>
        //                        3. List<DictionaryDataDTO> 通过 stream 流转成 List<DeviceDTO> 数据类型存储数据
        //涉及 house, tag_house, sys_dictionary_data 三表的数据调用, List<TagHouse> tagHouses 通过 houseId 查询 tag_house 查询出来的
        List<String> dataKeys = Arrays.stream(house.getDevices().split(","))
                .distinct()
                .collect(Collectors.toList());
        List<DictionaryDataDTO> dictionaryDataDTOS =  sysDictionaryService.getDicDataByKeys(dataKeys);
        List<DeviceDTO> devices = dictionaryDataDTOS.stream()
                .map(dataDTO ->{
                    DeviceDTO deviceDTO = new DeviceDTO();
                    deviceDTO.setDeviceCode(dataDTO.getDataKey());
                    deviceDTO.setDeviceName(dataDTO.getValue());
                    return deviceDTO;
                }).collect(Collectors.toList());
        houseDTO.setDevices(devices);

        // 处理 标签列表, 因为 LambdaQueryWrapper 的 in 方法只能查询同一张表的字段, 所以转为流对象再查
        List<String> tags = tagHouses.stream()
                .map(TagHouse::getTagCode)
                .collect(Collectors.toList());
        if(!CollectionUtils.isEmpty(tags)){
            List<Tag> tagDTOList = tagMapper.selectList(
                    new LambdaQueryWrapper<Tag>().in(Tag::getTagCode,tags));
            houseDTO.setTags(BeanCopyUtil.copyListProperties(tagDTOList, TagDTO::new));
        }

        return houseDTO;
    }


    /**
     * 增加标签-房源映射关系（批量）。
     *
     * @param houseId  房源 ID
     * @param tagCodes 标签码列表（需提前去重与有效性校验）
     */
    private void addTagHouses(Long houseId, List<String> tagCodes) {
        List<TagHouse> tagHouses = tagCodes.stream()
                .map(tagCode -> {
                    TagHouse tagHouse = new TagHouse();
                    tagHouse.setHouseId(houseId);
                    tagHouse.setTagCode(tagCode);
                    return tagHouse;
                })
                .collect(Collectors.toList());
        tagHouseMapper.insert(tagHouses);
    }

//    /**
//     * 增加标签-房源映射关系
//     *
//     * @param houseId  房源 ID
//     * @param tagCodes 标签码列表
//     */
//    private void addTagHouses(Long houseId, List<String> tagCodes) {
//        for (String tagCode : tagCodes) {
//            TagHouse tagHouse = new TagHouse();
//            tagHouse.setTagCode(tagCode);
//            tagHouse.setHouseId(houseId);
//            tagHouseMapper.insert(tagHouse);
//        }
//    }

    /**
     * 增加城市-房源映射关系（并写入城市房源列表缓存）。
     *
     * @param houseId  房源 ID
     * @param cityId   城市 ID
     * @param cityName 城市名称（冗余存储，便于查询）
     */
    private void addCityHouses(Long houseId, Long cityId, String cityName) {
        CityHouse cityHouse = new CityHouse();
        cityHouse.setHouseId(houseId);
        cityHouse.setCityId(cityId);
        cityHouse.setCityName(cityName);

        cityHouseMapper.insert(cityHouse);
        cacheCityHouse(1, houseId, null, cityId);
    }

    /**
     * 写入/更新城市-房源映射的缓存。
     *
     * @param op        操作类型：1-新增；2-迁移（老城市→新城市）
     * @param houseId   房源 ID
     * @param oldCityId 老城市 ID（op=2 时必传）
     * @param newCityId 新城市 ID
     * @throws RuntimeException Redis 操作失败时抛出（由上层事务处理）
     */
    private void cacheCityHouse(int op, Long houseId, Long oldCityId, Long newCityId) {
        try {
            if (op == 1) {
                // 追加到新城市列表
                redisService.setCacheList(CITY_HOUSE_PREFIX + newCityId, Arrays.asList(houseId));
            } else if (op == 2) {
                // 迁移：先从老城市移除，再追加到新城市
                redisService.removeForList(CITY_HOUSE_PREFIX + oldCityId, houseId);
                redisService.setCacheList(CITY_HOUSE_PREFIX + newCityId, Arrays.asList(houseId));
            } else {
                log.info("无效操作: 缓存城市房源关联信息");
            }
        } catch (Exception e) {
            log.info("缓存城市房源关联操作失败, houseId:{}, oldCityId:{}, newCityId:{}",
                    houseId, oldCityId, newCityId, e);
            throw e;
        }
    }

    /**
     * 组装 House 实体（仅从请求对象拷贝数据，不做持久化）。
     *
     * @param reqDTO 新增/编辑请求
     * @return House 持久化实体
     */
    private House setHouseInformation(HouseAddOrEditReqDTO reqDTO) {
        House house = new House();
        house.setUserId(reqDTO.getUserId());
        house.setTitle(reqDTO.getTitle());
        house.setRentType(reqDTO.getRentType());
        house.setFloor(reqDTO.getFloor());
        house.setAllFloor(reqDTO.getAllFloor());
        house.setHouseType(reqDTO.getHouseType());
        house.setRooms(reqDTO.getRooms());
        house.setPosition(reqDTO.getPosition());
        house.setArea(BigDecimal.valueOf(reqDTO.getArea()));
        house.setPrice(BigDecimal.valueOf(reqDTO.getPrice()));
        house.setIntro(reqDTO.getIntro());
        // 设备列表转 CSV 字符串（空串过滤）
        house.setDevices(
                reqDTO.getDevices().stream()
                        .filter(s -> !s.isEmpty())
                        .collect(Collectors.joining(","))
        );
        // 头图与图集（图集转 JSON）
        house.setHeadImage(reqDTO.getHeadImage());
        house.setImages(JsonUtil.obj2String(reqDTO.getImages()));

        // 行政区与定位信息
        house.setCityId(reqDTO.getCityId());
        house.setCityName(reqDTO.getCityName());
        house.setRegionId(reqDTO.getRegionId());
        house.setRegionName(reqDTO.getRegionName());
        house.setCommunityName(reqDTO.getCommunityName());
        house.setDetailAddress(reqDTO.getDetailAddress());
        house.setLongitude(BigDecimal.valueOf(reqDTO.getLongitude()));
        house.setLatitude(BigDecimal.valueOf(reqDTO.getLatitude()));
        return house;
    }

    /**
     * 新增/编辑请求的参数校验。
     * <ul>
     *   <li>房东存在性</li>
     *   <li>城市/区域层级与隶属关系</li>
     *   <li>标签有效性（去重后与库内一致）</li>
     *   <li>设备、出租方式、朝向、居室等字典项</li>
     *   <li>数值边界与结构关系（楼层/面积/经纬度等）</li>
     *   <li>房源重复（同房东 + 地址 + 标题）</li>
     * </ul>
     *
     * @param reqDTO 新增或编辑请求
     * @throws ServiceException 校验失败时抛出
     */
    private void checkAddOrEdit(HouseAddOrEditReqDTO reqDTO) {
        // 1. 房东校验
        AppUser appUser = appUserMapper.selectById(reqDTO.getUserId());
        if (appUser == null) {
            throw new ServiceException("房东信息错误", ResultCode.INVALID_PARA.getCode());
        }

        // 2. 行政区校验：存在 + 层级 + 隶属 + 名称回填
        SysRegion city = regionMapper.selectById(reqDTO.getCityId());
        SysRegion region = regionMapper.selectById(reqDTO.getRegionId());
        if (city == null || !Integer.valueOf(2).equals(city.getLevel())) fail("城市ID不合法");
        if (region == null || !Integer.valueOf(3).equals(region.getLevel())) fail("区域ID不合法");
        if (!region.getParentId().equals(city.getId())) fail("区域不属于该城市");
        reqDTO.setCityName(city.getName());
        reqDTO.setRegionName(region.getName());

        // 3. 标签码校验（去重后与库内一致）
        LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper<>();
        reqDTO.setTagCodes(reqDTO.getTagCodes().stream().distinct().collect(Collectors.toList()));
        queryWrapper.in(Tag::getTagCode, reqDTO.getTagCodes());
        List<Tag> tags = tagMapper.selectList(queryWrapper);
        if (tags == null || tags.isEmpty() || tags.size() != reqDTO.getTagCodes().size()) {
            throw new ServiceException("标签参数错误", ResultCode.INVALID_PARA.getCode());
        }

        // 4. 设备字典校验
        LambdaQueryWrapper<SysDictionaryData> qw = new LambdaQueryWrapper<>();
        qw.eq(SysDictionaryData::getTypeKey, "device_list")
                .in(SysDictionaryData::getDataKey, reqDTO.getDevices())
                .eq(SysDictionaryData::getStatus, 1);
        List<SysDictionaryData> sysDictionaryData = sysDictionaryDataMapper.selectList(qw);
        if (sysDictionaryData == null || sysDictionaryData.isEmpty()
                || sysDictionaryData.size() != reqDTO.getDevices().size()) {
            throw new ServiceException("设备参数错误", ResultCode.INVALID_PARA.getCode());
        }

        // 5. 字典字段：租赁类型 / 朝向 / 居室（户型如需校验可打开下一行）
        assertDict("rent_type_list", reqDTO.getRentType(), "出租类型参数错误");
//        assertDict("house_type_list", reqDTO.getHouseType(), "户型参数错误");
        assertDict("position_list", reqDTO.getPosition(), "朝向参数错误");
//        assertDict("rooms_num", reqDTO.getRooms(), "居室参数错误");

        // 6. 结构与数值合法性
        if (reqDTO.getFloor() < 1 || reqDTO.getFloor() > reqDTO.getAllFloor()) fail("楼层关系不合法");
        if (reqDTO.getArea() <= 0 || reqDTO.getArea() > 10000) fail("面积不合法");
        if (reqDTO.getPrice() < 0 || reqDTO.getPrice() > 1_000_000_000) fail("价格不合法");
        if (reqDTO.getTitle().length() > 50) fail("标题过长");
        if (reqDTO.getIntro().length() > 2047) fail("介绍过长");
        if (reqDTO.getLongitude() < -180 || reqDTO.getLongitude() > 180) fail("经度非法");
        if (reqDTO.getLatitude() < -90 || reqDTO.getLatitude() > 90) fail("纬度非法");

        // 7. 设备 CSV 长度限制
        String devicesCsv = String.join(",", reqDTO.getDevices().stream().distinct().toList());
        if (devicesCsv.length() > 255) fail("设备过多，超出存储上限");

//        // 8. 重复房源（同房东 + 详细地址 + 标题；编辑时排除自身）
//        boolean dup = houseMapper.exists(new LambdaQueryWrapper<House>()
//                .eq(House::getUserId, reqDTO.getUserId())
//                .eq(House::getDetailAddress, reqDTO.getDetailAddress())
//                .eq(House::getTitle, reqDTO.getTitle())
//                .ne(reqDTO.getHouseId() != null, House::getId, reqDTO.getHouseId()));
//        if (dup) fail("相同房源已存在");
    }

    /**
     * 字典项存在性断言（状态=1 有效）。
     *
     * @param typeKey 字典类型键（如 rent_type_list / position_list / rooms_num）
     * @param dataKey 字典数据键（请求传入的具体值）
     * @param msg     断言失败提示信息
     * @throws ServiceException 若不存在则抛出
     */
    private void assertDict(String typeKey, String dataKey, String msg) {
        boolean ok = sysDictionaryDataMapper.exists(new LambdaQueryWrapper<SysDictionaryData>()
                .eq(SysDictionaryData::getTypeKey, typeKey)
                .eq(SysDictionaryData::getDataKey, dataKey)
                .eq(SysDictionaryData::getStatus, 1));
        if (!ok) throw new ServiceException(msg, ResultCode.INVALID_PARA.getCode());
    }

    /**
     * 抛出业务异常的便捷方法。
     *
     * @param m 错误信息
     * @throws ServiceException 统一错误码 INVALID_PARA
     */
    private void fail(String m) {
        throw new ServiceException(m, ResultCode.INVALID_PARA.getCode());
    }

//    /**
//     * 定期检查已经出租的房源是否过期
//     */
//    @Scheduled(cron = "*/10 * * * * ?")
//    public void scheduleHouseStatus() {
//        log.info("定期检查已经出租的房源是否过期功能已开启");
//        String value = UUID.randomUUID().toString();
//        try {
//            // 加锁 if (lock) { ... } 如果 lock 是 null，会 NPE
//            // Boolean.TRUE.equals(lock) 等价于 lock != null && lock
//            Boolean lock = redisService.setCacheObjectIfAbsent(LOCK_KEY,value,180,TimeUnit.SECONDS);
//            if(Boolean.TRUE.equals(lock)){
//                // 在 house_status 查出全量, LambdaQueryWrapper 查出正在出租的房子
//                List<HouseStatus> rentingHouses = houseStatusMapper.selectList(
//                        new LambdaQueryWrapper<HouseStatus>()
//                                .eq(HouseStatus::getStatus, HouseStatusEnum.RENTING.name())
//                );
//
//                // 过滤需要修改的, houseId != null && 0 > calculateDifferenceMillis
//                List<HouseStatus> needConvertHouses = rentingHouses.stream()
//                        .filter(needConvertHouse -> needConvertHouse.getRentEndTime() != null
//                                && 0 > TimestampUtil.calculateDifferenceMillis(
//                                        TimestampUtil.getCurrentMillis(),needConvertHouse.getRentEndTime()))
//                        .collect(Collectors.toList());
//
//                // 修改房源状态信息, 该方法包含修改 mysql 和 redis
//                for(HouseStatus houseStatus : needConvertHouses){
//                    HouseStatusEditReqDTO reqDTO = new HouseStatusEditReqDTO();
//                    reqDTO.setHouseId(houseStatus.getHouseId());
//                    reqDTO.setStatus(HouseStatusEnum.UP.name());
//                    editStatus(reqDTO);
//                }
//
//            }else{
//                log.info("当前定时任务被其他实例执行");
//            }
//        }finally {
//            //解锁
//            redisService.cad(LOCK_KEY,value);
//        }
//    }


    /**
     * 定期检查已经出租的房源是否过期
     */
    // @Scheduled(cron = "*/10 * * * * ?")
    // 每天凌晨0点开始执行定时任务
    @Scheduled(cron = "0 0 0 * * ?")
    public void scheduleHouseStatus() {
        RLock lock = redissonLockService.acquire(LOCK_KEY,-1);
        if(lock == null){
            log.info("当前定时任务正在被其他实例执行");
            return;
        }
        try {
            // 在 house_status 查出全量, LambdaQueryWrapper 查出正在出租的房子
            List<HouseStatus> rentingHouses = houseStatusMapper.selectList(
                    new LambdaQueryWrapper<HouseStatus>()
                            .eq(HouseStatus::getStatus, HouseStatusEnum.RENTING.name())
            );

            // 过滤需要修改的, houseId != null && 0 > calculateDifferenceMillis
            List<HouseStatus> needConvertHouses = rentingHouses.stream()
                    .filter(needConvertHouse -> needConvertHouse.getRentEndTime() != null
                            && 0 > TimestampUtil.calculateDifferenceMillis(
                            TimestampUtil.getCurrentMillis(),needConvertHouse.getRentEndTime()))
                    .collect(Collectors.toList());

            // 修改房源状态信息, 该方法包含修改 mysql 和 redis
            for(HouseStatus houseStatus : needConvertHouses){
                HouseStatusEditReqDTO reqDTO = new HouseStatusEditReqDTO();
                reqDTO.setHouseId(houseStatus.getHouseId());
                reqDTO.setStatus(HouseStatusEnum.UP.name());
                editStatus(reqDTO);
            }
        }finally {
            //解锁
            if(lock.isLocked() && lock.isHeldByCurrentThread()){
                redissonLockService.releaseLock(lock);
            }
        }
    }

}
